<?
	/**
	 * UCLibMSN.php
	 * SDO UC项目公共类库 - MSN操作
	 *
	 * Copyright (c) 2010 snda.com - Web Development Center
	 * @author: From Internet
	 * @modifier: Gu Shougang <gushougang@snda.com>
	 */

	class UCLibMSN
	{
		//dispatch服务器地址及端口
		private $server = 'messenger.hotmail.com';
		private $port = 1863;

		private $passport_url = 'https://login.live.com/RST.srf';

		private $prod_key = 'PK}_A_0N_K%O?A9S';
		private $prod_id = 'PROD0114ES4Z%Q5W';

		private $clientid = '0x7000800C';

		private $id;
		private $fp = false;
		private $error = '';

		private $authed = false;
		private $user = '';
		private $password = '';

		private $passport_policy = '';
		private $oim_try = 3;
		private $oim_ticket = '';
		private $contact_ticket = '';

		private $debug = false;

		private $timeout = 15;
		private $stream_timeout = 2;

		private $sb;
		private $font_fn = 'Arial';
		private $font_co = '333333';
		private $font_ef = '';

		private $max_msn_message_len = 1664;
		private $max_yahoo_message_len = 518;

		/**
		* 构造函数,需要以下PHP扩展的支持curl,pcre,mcrypt,bcmath
		* 
		* @return UCLibMSN
		*/
		public function UCLibMSN()
		{
			if(!function_exists('curl_init'))
			{
				die("We need curl module!\n");
			}
			
			if(!function_exists('preg_match'))
			{
				die("We need pcre module!\n");
			}
			
			if(!function_exists('mcrypt_cbc'))
			{
				die("We need mcrypt module !\n");
			}
		}


	    /**
	     * 登录指定MSN账号
	     *
	     * @param string $user
	     * @param string $password
	     * @return bool
	     */
		public function connect($user, $password)
		{
			$this->id = 1;

			$server='messenger.hotmail.com';
			$port=1863;

			$errno=0;
			$errstr='';
			$stream_timeout = 2;

			$this->fp = @fsockopen($server, $port, $errno, $errstr, 5);
			if(!$this->fp)
			{
				$this->error = "Can't connect to $server:$port, error => $errno, $errstr";
				return false;
			}

			stream_set_timeout($this->fp, $stream_timeout);
			$this->authed = false;

			$this->writeln("VER $this->id MSNP15 CVR0");

			$start_tm = time();
			while(!feof($this->fp))
			{
				$data = $this->readln();
				// no data?
				if($data === false)
				{
					if($this->timeout > 0)
					{
						$now_tm = time();
						$used_time = ($now_tm >= $start_tm) ? $now_tm - $start_tm : $now_tm;
						
						if($used_time > $this->timeout)
						{
							// logout now
							// NS: >>> OUT
							$this->writeln("OUT");
							fclose($this->fp);
							$this->error = 'Timeout, maybe protocol changed!';
							$this->debug_message("*** $this->error");
							return false;
						}
					}
					
					continue;
				}
				
			$code = substr($data, 0, 3);
			$start_tm = time();
			switch($code)
			{
				case 'VER':
					$this->writeln("CVR $this->id 0x0409 winnt 5.1 i386 MSMSGS 8.1.0178 msmsgs $user");
					break;
				case 'CVR':
					$this->writeln("USR $this->id SSO I $user");
					break;
				case 'USR':
					if($this->authed)
					{
						return true;
					}

					// max. 16 digits for password
					if(strlen($password) > 16)
					{
						$password = substr($password, 0, 16);
					}

					$this->user = $user;
					$this->password = $password;

					// NS: <<< USR {id} SSO S {policy} {nonce}
					@list(/* USR */, /* id */, /* SSO */, /* S */, $policy, $nonce,) = @explode(' ', $data);

					$this->passport_policy = $policy;
					$aTickets = $this->get_passport_ticket();
					
					if(!$aTickets || !is_array($aTickets))
					{
						// logout now
						// NS: >>> OUT
						$this->writeln("OUT");
						fclose($this->fp);
						$this->error = 'Passport authenticated fail!';
						$this->debug_message("*** $this->error");
						return false;
					}

					$this->oim_ticket = $aTickets['oim_ticket'];
					$this->contact_ticket = $aTickets['contact_ticket'];

					// NS: >>> USR {id} SSO S {ticket} {login_code}
					$this->writeln("USR $this->id SSO S ".$aTickets['ticket']." ".$this->generateLoginBLOB($aTickets['secret'], $nonce));
					
					$this->authed = true;
					break;
				case 'XFR':
					@list(/* XFR */, /* id */, /* NS */, $server, /* ... */) = @explode(' ', $data);
					@list($ip, $port) = @explode(':', $server);
					// this connection will close after XFR
					fclose($this->fp);

					$errno=0;
					$errstr='';
					$this->fp = @fsockopen($ip, $port, $errno, $errstr, 5);
					
					if(!$this->fp)
					{
						$this->error = "Can't connect to $ip:$port, error => $errno, $errstr";
						$this->debug_message("*** $this->error");
						return false;
					}

					stream_set_timeout($this->fp, $this->stream_timeout);
					$this->writeln("VER $this->id MSNP15 CVR0");
					break;
				case 'GCF':
					// return some policy data after 'USR {id} SSO I {user}' command
					// NS: <<< GCF 0 {size}
					@list(/* GCF */, /* 0 */, $size,) = @explode(' ', $data);
					// we don't need the data, just read it and drop
					if(is_numeric($size) && $size > 0)
					{
						$this->readdata($size);
					}
					break;
				default:
					// we'll quit if got any error
					if(is_numeric($code))
					{
						// logout now
						// NS: >>> OUT
						$this->writeln("OUT");
						fclose($this->fp);
						$this->error = "Error code: $code, please check the detail information from: http://msnpiki.msnfanatic.com/index.php/Reference:Error_List";
						$this->debug_message("*** $this->error");
						return false;
					}
					// unknown response from server, just ignore it
					break;
				}
			}
			// never goto here
			return false;
		}


		/**
		* 获取通行证的令牌
		*
		* @param string $url
		* @return string
		*/
		private function get_passport_ticket($url = '')
		{
			$user = $this->user;
			$password = htmlspecialchars($this->password);

			if($url === '')
			{
				$passport_url = $this->passport_url;
			}
			else
			{
				$passport_url = $url;
			}

			$XML = '<?xml version="1.0" encoding="UTF-8"?>
			<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/"
			xmlns:wsse="http://schemas.xmlsoap.org/ws/2003/06/secext"
			xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion"
			xmlns:wsp="http://schemas.xmlsoap.org/ws/2002/12/policy"
			xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
			xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/03/addressing"
			xmlns:wssc="http://schemas.xmlsoap.org/ws/2004/04/sc"
			xmlns:wst="http://schemas.xmlsoap.org/ws/2004/04/trust">
			<Header>
			<ps:AuthInfo xmlns:ps="http://schemas.microsoft.com/Passport/SoapServices/PPCRL" Id="PPAuthInfo">
			<ps:HostingApp>{7108E71A-9926-4FCB-BCC9-9A9D3F32E423}</ps:HostingApp>
			<ps:BinaryVersion>4</ps:BinaryVersion>
			<ps:UIVersion>1</ps:UIVersion>
			<ps:Cookies></ps:Cookies>
			<ps:RequestParams>AQAAAAIAAABsYwQAAAAxMDMz</ps:RequestParams>
			</ps:AuthInfo>
			<wsse:Security>
			<wsse:UsernameToken Id="user">
			<wsse:Username>' . $user . '</wsse:Username>
			<wsse:Password>' . $password . '</wsse:Password>
			</wsse:UsernameToken>
			</wsse:Security>
			</Header>
			<Body>
			<ps:RequestMultipleSecurityTokens xmlns:ps="http://schemas.microsoft.com/Passport/SoapServices/PPCRL" Id="RSTS">
			<wst:RequestSecurityToken Id="RST0">
			<wst:RequestType>http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue</wst:RequestType>
			<wsp:AppliesTo>
			<wsa:EndpointReference>
			<wsa:Address>http://Passport.NET/tb</wsa:Address>
			</wsa:EndpointReference>
			</wsp:AppliesTo>
			</wst:RequestSecurityToken>
			<wst:RequestSecurityToken Id="RST1">
			<wst:RequestType>http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue</wst:RequestType>
			<wsp:AppliesTo>
			<wsa:EndpointReference>
			<wsa:Address>messengerclear.live.com</wsa:Address>
			</wsa:EndpointReference>
			</wsp:AppliesTo>
			<wsse:PolicyReference URI="' . $this->passport_policy . '"></wsse:PolicyReference>
			</wst:RequestSecurityToken>
			<wst:RequestSecurityToken Id="RST2">
			<wst:RequestType>http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue</wst:RequestType>
			<wsp:AppliesTo>
			<wsa:EndpointReference>
			<wsa:Address>messenger.msn.com</wsa:Address>
			</wsa:EndpointReference>
			</wsp:AppliesTo>
			<wsse:PolicyReference URI="?id=507"></wsse:PolicyReference>
			</wst:RequestSecurityToken>
			<wst:RequestSecurityToken Id="RST3">
			<wst:RequestType>http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue</wst:RequestType>
			<wsp:AppliesTo>
			<wsa:EndpointReference>
			<wsa:Address>contacts.msn.com</wsa:Address>
			</wsa:EndpointReference>
			</wsp:AppliesTo>
			<wsse:PolicyReference URI="MBI"></wsse:PolicyReference>
			</wst:RequestSecurityToken>
			<wst:RequestSecurityToken Id="RST4">
			<wst:RequestType>http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue</wst:RequestType>
			<wsp:AppliesTo>
			<wsa:EndpointReference>
			<wsa:Address>messengersecure.live.com</wsa:Address>
			</wsa:EndpointReference>
			</wsp:AppliesTo>
			<wsse:PolicyReference URI="MBI_SSL"></wsse:PolicyReference>
			</wst:RequestSecurityToken>
			<wst:RequestSecurityToken Id="RST5">
			<wst:RequestType>http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue</wst:RequestType>
			<wsp:AppliesTo>
			<wsa:EndpointReference>
			<wsa:Address>spaces.live.com</wsa:Address>
			</wsa:EndpointReference>
			</wsp:AppliesTo>
			<wsse:PolicyReference URI="MBI"></wsse:PolicyReference>
			</wst:RequestSecurityToken>
			<wst:RequestSecurityToken Id="RST6">
			<wst:RequestType>http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue</wst:RequestType>
			<wsp:AppliesTo>
			<wsa:EndpointReference>
			<wsa:Address>storage.msn.com</wsa:Address>
			</wsa:EndpointReference>
			</wsp:AppliesTo>
			<wsse:PolicyReference URI="MBI"></wsse:PolicyReference>
			</wst:RequestSecurityToken>
			</ps:RequestMultipleSecurityTokens>
			</Body>
			</Envelope>';

			$curl = curl_init();
			curl_setopt($curl, CURLOPT_URL, $passport_url);
			if($this->debug)
			{
				curl_setopt($curl, CURLOPT_HEADER, 1);
			}
			curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
			curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
			curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
			curl_setopt($curl, CURLOPT_POST, 1);
			curl_setopt($curl, CURLOPT_POSTFIELDS, $XML);
			$data = curl_exec($curl);
			$http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
			curl_close($curl);

			if($http_code != 200)
			{
				// sometimes, redirect to another URL
				// MSNP15
				//<faultcode>psf:Redirect</faultcode>
				//<psf:redirectUrl>https://msnia.login.live.com/pp450/RST.srf</psf:redirectUrl>
				//<faultstring>Authentication Failure</faultstring>
				if(strpos($data, '<faultcode>psf:Redirect</faultcode>') === false)
				{
					$this->debug_message("*** Can't get passport ticket! http code = $http_code");
					return false;
				}
				preg_match("#<psf\:redirectUrl>(.*)</psf\:redirectUrl>#", $data, $matches);
				if(count($matches) == 0)
				{
					$this->debug_message("*** redirect, but can't get redirect URL!");
					return false;
				}
				$redirect_url = $matches[1];
				if($redirect_url == $passport_url)
				{
					$this->debug_message("*** redirect, but redirect to same URL!");
					return false;
				}
				$this->debug_message("*** redirect to $redirect_url");
				return $this->get_passport_ticket($redirect_url);
			}

			// sometimes, rediret to another URL, also return 200
			// MSNP15
			//<faultcode>psf:Redirect</faultcode>
			//<psf:redirectUrl>https://msnia.login.live.com/pp450/RST.srf</psf:redirectUrl>
			//<faultstring>Authentication Failure</faultstring>
			if(strpos($data, '<faultcode>psf:Redirect</faultcode>') !== false)
			{
				preg_match("#<psf\:redirectUrl>(.*)</psf\:redirectUrl>#", $data, $matches);
				
				if(count($matches) != 0)
				{
					$redirect_url = $matches[1];
					if($redirect_url == $passport_url)
					{
						$this->debug_message("*** redirect, but redirect to same URL!");
						return false;
					}
					
					$this->debug_message("*** redirect to $redirect_url");
					return $this->get_passport_ticket($redirect_url);
				}
			}

			preg_match("#".
			"<wsse\:BinarySecurityToken Id=\"Compact1\">(.*)</wsse\:BinarySecurityToken>(.*)".
			"<wst\:BinarySecret>(.*)</wst\:BinarySecret>(.*)".
			"<wsse\:BinarySecurityToken Id=\"PPToken2\">(.*)</wsse\:BinarySecurityToken>(.*)".
			"<wsse\:BinarySecurityToken Id=\"Compact3\">(.*)</wsse\:BinarySecurityToken>(.*)".
			"<wsse\:BinarySecurityToken Id=\"Compact4\">(.*)</wsse\:BinarySecurityToken>(.*)".
			"<wsse\:BinarySecurityToken Id=\"Compact5\">(.*)</wsse\:BinarySecurityToken>(.*)".
			"<wsse\:BinarySecurityToken Id=\"Compact6\">(.*)</wsse\:BinarySecurityToken>(.*)".
			"#", $data, $matches);

			// no ticket found!
			if(count($matches) == 0)
			{
				$this->debug_message("*** Can't get passport ticket!");
				return false;
			}

			// yes, we get ticket
			$aTickets = array(
				'ticket' => html_entity_decode($matches[1]),
				'secret' => html_entity_decode($matches[3]),
				'contact_ticket' => html_entity_decode($matches[7]),
				'oim_ticket' => html_entity_decode($matches[9]),
			);
			return $aTickets;
		}


		/**
		* 获取MSN联系人包括分组信息
		*
		* @return array
		*      email=>array(
		*          email=><string>
		*          name=><string>
		*          group=><string>
		*      )
		*/
		public function getAddressBook()
		{
			//SOAP请求包
			$XML='<soap:Envelope xmlns:soap=\'http://schemas.xmlsoap.org/soap/envelope/\'
				xmlns:xsi=\'http://www.w3.org/2001/XMLSchema-instance\'
				xmlns:xsd=\'http://www.w3.org/2001/XMLSchema\'
				xmlns:soapenc=\'http://schemas.xmlsoap.org/soap/encoding/\'>
				<soap:Header>
					<ABApplicationHeader xmlns=\'http://www.msn.com/webservices/AddressBook\'>
					     <ApplicationId>CFE80F9D-180F-4399-82AB-413F33A1FA11</ApplicationId>
					     <IsMigration>false</IsMigration>
					     <PartnerScenario>Initial</PartnerScenario>
					</ABApplicationHeader>
					<ABAuthHeader xmlns=\'http://www.msn.com/webservices/AddressBook\'>
					     <ManagedGroupRequest>false</ManagedGroupRequest>
					     <TicketToken>' . htmlspecialchars($this->contact_ticket) . '</TicketToken>
					</ABAuthHeader>
				</soap:Header>
				<soap:Body>
					<ABFindAll xmlns=\'http://www.msn.com/webservices/AddressBook\'>
					     <abId>00000000-0000-0000-0000-000000000000</abId>
					     <abView>Full</abView>
					     <deltasOnly>false</deltasOnly>
					     <lastChange>0001-01-01T00:00:00.0000000-08:00</lastChange>
					</ABFindAll>
				</soap:Body>
			</soap:Envelope>';
			
			$header_array = array(
				'SOAPAction: http://www.msn.com/webservices/AddressBook/ABFindAll',
				'Content-Type: text/xml; charset=utf-8',
				'Cookie: MSPAuth=Removed',
				'Host: contacts.msn.com'
			);

			//发送SOAP请求
			$curl = curl_init();
			curl_setopt($curl, CURLOPT_URL,'http://contacts.msn.com/abservice/abservice.asmx');
			curl_setopt($curl, CURLOPT_HTTPHEADER, $header_array);
			curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
			curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
			curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
			if($this->debug)
			{
				curl_setopt($curl, CURLOPT_HEADER, 1);
			}
			curl_setopt($curl, CURLOPT_POST, 1);
			curl_setopt($curl, CURLOPT_POSTFIELDS, $XML);
			$data = curl_exec($curl);
			$http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
			curl_close($curl);
			//$this->debug_message("*** Get Result:\n$data");

			if($http_code != 200)
			{
				return array();
			}

			//将回应包转换为对象
			$start = strpos($data, '<ABFindAllResult>');
			$end = strpos($data, '</ABFindAllResult>');
			$start += 17;
			$result = substr($data, $start, $end - $start);
			//$this->debug_message("*** Get Result:\n$result");

			$result = simplexml_load_string("<?xml version='1.0'?><document>" . $result . "</document>");

			//获取分组信息
			$groups = array();
			foreach($result->groups->Group as $group)
			{
				$groups[(string)$group->groupId] = (string)$group->groupInfo->name;
			}
			
			$groups[0] = '未分组联系人';
			
			//var_dump($result);

			//获取联系人信息
			$contacts = array();
			foreach($result->contacts->Contact as $contact)
			{
				$info = $contact->contactInfo;
				//自己，不加入联系人中
				if($info->contactType == 'Me')
				{
					continue;
				}

				//获取所属的组
				$email = $name = $group = '';
				if(isset($info->groupIds))
				{
					if(isset($info->groupIds->guid))
					{
						$group = $groups[(string)$info->groupIds->guid];
					}
				}
				else
				{
					$group = $groups[0];
				}

				//获取EMAIL地址
				if(isset($info->passportName))
				{
					$email = $info->passportName;
				}
				elseif(isset($info->emails) && isset($info->emails->ContactEmail))
				{
					if(is_array($info->emails->ContactEmail))
					{
						$email = $info->emails->ContactEmail[0]->email;
					}
					else
					{
						$email = $info->emails->ContactEmail->email;
					}
				}

				//获取显示名
				if(isset($info->annotations))
				{
					if(isset($info->annotations->Annotation))
					{
						if(is_array($info->annotations->Annotation))
						{
							foreach($info->annotations->Annotation as $ann)
							{
								if($ann->Name == 'AB.NickName')
								{
									$name = $ann->Value;
									break;
								}
							}
						}
						elseif($info->annotations->Annotation->Name == 'AB.NickName')
						{
							$name = $info->annotations->Annotation->Value;
						}
					}
				}
				
				if(!$name)
				{
					if(isset($info->displayName))
					{
						$name = $info->displayName;
					}
				}
		
				if(!$name)
				{
					if(isset($info->quickName))
					{
						$name = $info->quickName;
					}
				}
				
				if(!$name)
				{
					$name = $email;
				}

				//加入联系人数组中
				if(strlen((string)$email))
				{
					$contacts[(string)$email]=array('email'=>(string)$email,'name'=>(string)$name,'group'=>$group);
				}
			}
			
			//var_dump($contacts);
			
			return $contacts;
		}

		private function derive_key($key, $magic)
		{
			$hash1 = hash_hmac('sha1', $magic, $key, true);
			$hash2 = hash_hmac('sha1', $hash1.$magic, $key, true);
			$hash3 = hash_hmac('sha1', $hash1, $key, true);
			$hash4 = hash_hmac('sha1', $hash3.$magic, $key, true);
			return $hash2 . substr($hash4, 0, 4);
		}

		private function generateLoginBLOB($key, $challenge)
		{
			$key1 = base64_decode($key);
			$key2 = $this->derive_key($key1, 'WS-SecureConversationSESSION KEY HASH');
			$key3 = $this->derive_key($key1, 'WS-SecureConversationSESSION KEY ENCRYPTION');

			// get hash of challenge using key2
			$hash = hash_hmac('sha1', $challenge, $key2, true);
			// get 8 bytes random data
			$iv = substr(base64_encode(rand(1000, 9999) . rand(1000, 9999)), 2, 8);

			$cipher = mcrypt_cbc(MCRYPT_3DES, $key3, $challenge . "\x08\x08\x08\x08\x08\x08\x08\x08", MCRYPT_ENCRYPT, $iv);

			$blob = pack('LLLLLLL', 28, 1, 0x6603, 0x8004, 8, 20, 72);
			$blob .= $iv;
			$blob .= $hash;
			$blob .= $cipher;

			return base64_encode($blob);
		}

		public function sendMessage($sMessage, $aTo)
		{
			if(!function_exists('bcmod'))
			{
				die("We need bcmath module !\n");
			}
		
			if(!is_array($aTo))
			{
				if($aTo === '')
				{
					$aTo = array();
				}
				else
				{
					$aTo = array($aTo);
				}
			}
		
			$this->writeln("CHG $this->id NLN");
			stream_set_timeout($this->fp, $this->stream_timeout);
			$quit = false;
			$online_cnt = 0;
			$offline_cnt = 0;
			$other_cnt = 0;
			$start_tm = time();
		
			while(!feof($this->fp))
			{
				if($quit)
				{
					break;
				}
				$data = $this->readln();
				// no data ?
				if($data === false)
				{
					if($this->timeout > 0)
					{
						$now_tm = time();
						$used_time = ($now_tm >= $start_tm) ? $now_tm - $start_tm : $now_tm;
						if($used_time > $this->timeout)
						{
							$this->error = 'Timeout, maybe protocol changed!';
							$this->debug_message("*** $this->error");
							break;
						}
					}
					continue;
				}
				
				$code = substr($data, 0, 3);
				$start_tm = time();
				
				switch($code)
				{
					case 'SBS':
						//$this->writeln("CHG $this->id NLN");
						break;
					case 'MSG':
						@list(/* MSG */, /* Hotmail */, /* Hotmail */, $size,) = @explode(' ', $data);
						// we don't need the notification data, so just ignore it
						if(is_numeric($size) && $size > 0)
						{
							$this->readdata($size);
						}
						break;
					case 'CHL':
						@list(/* CHL */, /* 0 */, $chl_code,) = @explode(' ', $data);
						$fingerprint = $this->getChallenge($chl_code);
						// NS: >>> QRY {id} {product_id} 32
						// NS: >>> fingerprint
						$this->writeln("QRY $this->id $this->prod_id 32");
						$this->writedata($fingerprint);
						break;
					case 'SYN':
						break;
					case 'CHG':
						if(count($aTo) == 0 || $sMessage === '')
						{
							$quit = true;
							break;
						}

						$aMSNUsers = array();
						$aOfflineUsers = array();
						$aOtherUsers = array();
						$nMSNUsers = 0;
						foreach($aTo as $sUser)
						{
							@list($u_user, $u_domain, $u_network) = @explode('@', $sUser);
							if($u_network === '' || $u_network == NULL)
							{
								$u_network = 1;
							}
							
							$to_email = trim($u_user . '@' . $u_domain);
							if($u_network == 1)
							{
								$aMSNUsers[$nMSNUsers++] = $to_email;
							}
							else
							{
								$aOtherUsers[$u_network][] = $to_email;
							}
						}

						if($nMSNUsers == 0)
						{
							// no MSN account, only others
							// process other network first
							foreach($aOtherUsers as $network => $aNetUsers)
							{
							    $other_cnt++;
							    $aMessage = $this->getMessage($sMessage, $network);
							    foreach($aNetUsers as $to)
							    {
							        foreach($aMessage as $message)
							        {
							            $len = strlen($message);
							            $this->writeln("UUM $this->id $to $network 1 $len");
							            $this->writedata($message);
							        }
							        $this->debug_message("*** sent to $to (network: $network):\n$sMessage");
							    }
							}
							$quit = true;
							break;
						}

						$nCurrentUser = 0;

						// okay, try to ask a switchboard (SB) for sending message
						// NS: >>> XFR {id} SB
						$this->writeln("XFR $this->id SB");
						break;
					case 'XFR':
						// NS: <<< XFR {id} SB {server} CKI {cki} U messenger.msn.com 0
						@list(/* XFR */, /* {id} */, /* SB */, $server, /* CKI */, $cki_code, /* ... */) = @explode(' ', $data);
						@list($ip, $port) = @explode(':', $server);
						$bSBresult = $this->switchboard_control($ip, $port, $cki_code, $aMSNUsers[$nCurrentUser], $sMessage);
						if($bSBresult === false)
						{
							// error for switchboard
							$this->debug_message("!!! error for sending message to ".$aMSNUsers[$nCurrentUser]);
							$aOfflineUsers[] = $aMSNUsers[$nCurrentUser];
						}
						else
						{
							$online_cnt++;
						}

						$nCurrentUser++;
						if($nCurrentUser < $nMSNUsers)
						{
							// for next user
							// okay, try to ask a switchboard (SB) for sending message
							// NS: >>> XFR {id} SB
							$this->writeln("XFR $this->id SB");
							continue;
						}

						// okay, process offline user
						$lockkey = '';
						$re_login = false;
						foreach($aOfflineUsers as $to)
						{
							$offline_cnt++;
							for($i = 0; $i < $this->oim_try; $i++)
							{
		    						$oim_result = $this->sendOIM($to, $sMessage, $lockkey);
		   						if($oim_result === true)
		   						{
									// finished
									break;
								}
								if(is_array($oim_result) && $oim_result['challenge'] !== false)
								{
									// need challenge lockkey
									$lockkey = $this->getChallenge($oim_result['challenge']);
								}
								if($oim_result === false || $oim_result['auth_policy'] !== false)
								{
									if($re_login)
									{
										$this->debug_message("*** can't send OIM, but we already re-login again, so ignore this OIM");
										break;
									}
									
									$this->debug_message("*** can't send OIM, maybe ticket expired, try to login again");
									// maybe we need to re-login again
									$aTickets = $this->get_passport_ticket();
									if(!$aTickets || !is_array($aTickets))
									{
										// failed to login? ignore it
										$this->debug_message("*** can't re-login, something wrong here, ignore this OIM");
										break;
									}
									
									$re_login = true;
									$this->oim_ticket = $aTickets['oim_ticket'];
									$this->contact_ticket = $aTickets['contact_ticket'];
									$this->debug_message("**** get new ticket, try it again");
								}
							}
						}
						$quit = true;
						break;
					case 'LST':
						// NS: <<< LST {email} {alias} 11 0
						@list(/* LST */, $email, $alias, ) = @explode(' ', $data);
						$this->friends[$email]=$alias;
						break;
					default:
						if(is_numeric($code))
						{
							$this->error = "Error code: $code, please check the detail information from: http://msnpiki.msnfanatic.com/index.php/Reference:Error_List";
							$this->debug_message("*** NS: $this->error");
						}
						break;
				}
			}
			// logout now
			// NS: >>> OUT
			$this->writeln("OUT");
			fclose($this->fp);
			
			return array(
				'online' => $online_cnt,
				'offline' => $offline_cnt,
				'others' => $other_cnt
			);
		}

		function getChallenge($code)
		{
			// MSNP15
			// http://msnpiki.msnfanatic.com/index.php/MSNP11:Challenges
			// Step 1: The MD5 Hash
			$md5Hash = md5($code . $this->prod_key);
			$aMD5 = @explode("\0", chunk_split($md5Hash, 8, "\0"));
			for($i = 0; $i < 4; $i++)
			{
				$aMD5[$i] = implode('', array_reverse(@explode("\0", chunk_split($aMD5[$i], 2, "\0"))));
				$aMD5[$i] = (0 + base_convert($aMD5[$i], 16, 10)) & 0x7FFFFFFF;
			}

			// Step 2: A new string
			$chl_id = $code . $this->prod_id;
			$chl_id .= str_repeat('0', 8 - (strlen($chl_id) % 8));

			$aID = @explode("\0", substr(chunk_split($chl_id, 4, "\0"), 0, -1));
			for($i = 0; $i < count($aID); $i++)
			{
				$aID[$i] = implode('', array_reverse(@explode("\0", chunk_split($aID[$i], 1, "\0"))));
				$aID[$i] = 0 + base_convert(bin2hex($aID[$i]), 16, 10);
			}

			// Step 3: The 64 bit key
			$magic_num = 0x0E79A9C1;
			$str7f = 0x7FFFFFFF;
			$high = 0;
			$low = 0;
			for($i = 0; $i < count($aID); $i += 2)
			{
				$temp = $aID[$i];
				$temp = bcmod(bcmul($magic_num, $temp), $str7f);
				$temp = bcadd($temp, $high);
				$temp = bcadd(bcmul($aMD5[0], $temp), $aMD5[1]);
				$temp = bcmod($temp, $str7f);

				$high = $aID[$i+1];
				$high = bcmod(bcadd($high, $temp), $str7f);
				$high = bcadd(bcmul($aMD5[2], $high), $aMD5[3]);
				$high = bcmod($high, $str7f);

				$low = bcadd(bcadd($low, $high), $temp);
			}

			$high = bcmod(bcadd($high, $aMD5[1]), $str7f);
			$low = bcmod(bcadd($low, $aMD5[3]), $str7f);

			$new_high = bcmul($high & 0xFF, 0x1000000);
			$new_high = bcadd($new_high, bcmul($high & 0xFF00, 0x100));
			$new_high = bcadd($new_high, bcdiv($high & 0xFF0000, 0x100));
			$new_high = bcadd($new_high, bcdiv($high & 0xFF000000, 0x1000000));
			// we need integer here
			$high = 0 + $new_high;

			$new_low = bcmul($low & 0xFF, 0x1000000);
			$new_low = bcadd($new_low, bcmul($low & 0xFF00, 0x100));
			$new_low = bcadd($new_low, bcdiv($low & 0xFF0000, 0x100));
			$new_low = bcadd($new_low, bcdiv($low & 0xFF000000, 0x1000000));
			// we need integer here
			$low = 0 + $new_low;

			// we just use 32 bits integer, don't need the key, just high/low
			// $key = bcadd(bcmul($high, 0x100000000), $low);

			// Step 4: Using the key
			$md5Hash = md5($code . $this->prod_key);
			$aHash = @explode("\0", chunk_split($md5Hash, 8, "\0"));

			$hash = '';
			$hash .= sprintf("%08x", (0 + base_convert($aHash[0], 16, 10)) ^ $high);
			$hash .= sprintf("%08x", (0 + base_convert($aHash[1], 16, 10)) ^ $low);
			$hash .= sprintf("%08x", (0 + base_convert($aHash[2], 16, 10)) ^ $high);
			$hash .= sprintf("%08x", (0 + base_convert($aHash[3], 16, 10)) ^ $low);

			return $hash;
		}

		private function getMessage($sMessage, $network = 1)
		{
			$msg_header = "MIME-Version: 1.0\r\nContent-Type: text/plain; charset=UTF-8\r\nX-MMS-IM-Format: FN=$this->font_fn; EF=$this->font_ef; CO=$this->font_co; CS=0; PF=22\r\n\r\n";
			$msg_header_len = strlen($msg_header);
			
			if ($network == 1)
			{
				$maxlen = $this->max_msn_message_len - $msg_header_len;
			}
			else
			{
				$maxlen = $this->max_yahoo_message_len - $msg_header_len;
			}
			
			$aMessage = array();
			$aStr = @explode("\n", $sMessage);
			$cur_len = 0;
			$msg = '';
			$add_crlf = false;
			
			foreach($aStr as $str)
			{
				$str = str_replace("\r", '', $str);
				$len = strlen($str);
				while($len > $maxlen)
				{
					if($cur_len > 0)
					{
						// already has header/msg
						$aMessage[] = $msg_header.$msg;
						$cur_len = 0;
						$msg = '';
						$add_crlf = false;
					}
					$aMessage[] = $msg_header.substr($str, 0, $maxlen);
					$str = substr($str, $maxlen);
					$len = strlen($str);
				}
				if(($cur_len + $len) > $maxlen)
				{
					$aMessage[] = $msg_header.$msg;
					$cur_len = 0;
					$msg = '';
					$add_crlf = false;
				}
				if($msg !== '' || $add_crlf)
				{
					$msg .= "\r\n";
					$cur_len += 2;
				}
				$add_crlf = true;
				$msg .= $str;
				$cur_len += $len;
			}
			
			if($cur_len != 0)
			{
				$aMessage[] = $msg_header.$msg;
			}
			
			return $aMessage;
		}


		private function switchboard_control($ip, $port, $cki_code, $sTo, $sMessage)
		{
			$this->debug_message("*** SB: try to connect to switchboard server $ip:$port");
			$this->sb = @fsockopen($ip, $port, $errno, $errstr, 5);
			
			if(!$this->sb)
			{
				$this->error = "SB: Can't connect to $ip:$port, error => $errno, $errstr";
				$this->debug_message("*** $this->error");
				return false;
			}

			$user = $sTo;
			stream_set_timeout($this->sb, $this->stream_timeout);
			// SB: >>> USR {id} {user} {cki}
			$this->sb_writeln("USR $this->id $this->user $cki_code");

			$sent = false;
			$start_tm = time();
			$got_error = false;
			$offline = false;
			
			while(!feof($this->sb))
			{
				if($sent || $offline)
				{
					break;
				}
				
				$data = $this->sb_readln();
				
				if($data === false)
				{
					if($this->timeout > 0)
					{
						$now_tm = time();
						$used_time = ($now_tm >= $start_tm) ? $now_tm - $start_tm : $now_tm;
						
						if($used_time > $this->timeout)
						{
							$this->error = 'Timeout, maybe protocol changed!';
							$this->debug_message("*** $this->error");
							break;
						}
					}
					
					continue;
				}
				
				$code = substr($data, 0, 3);
				$start_tm = time();
				
				switch($code)
				{
					case 'USR':
						$this->sb_writeln("CAL $this->id $user");
						break;
					case 'CAL':
						break;
					case '217':
						$this->debug_message("*** SB: $user offline! skip to send message!");
						$offline = true;
						break;
					case 'JOI':
						$aMessage = $this->getMessage($sMessage);
					
						foreach($aMessage as $message)
						{
							$len = strlen($message);
							$this->sb_writeln("MSG 20 N $len");
							$this->sb_writedata($message);
						}
						
						$sent = true;
						break;
					default:
						if(is_numeric($code))
						{
							$this->error = "Error code: $code, please check the detail information from: http://msnpiki.msnfanatic.com/index.php/Reference:Error_List";
							$this->debug_message("*** SB: $this->error");
							$got_error = true;
						}
						break;
				}
			}
			if(feof($this->sb))
			{
				// lost connection? error? try OIM later
				@fclose($this->sb);
				return false;
			}
			$this->sb_writeln("OUT");
			@fclose($this->sb);
			if($offline || $got_error)
			{
				return false;
			}
			return true;
		}

		private function sendOIM($to, $sMessage, $lockkey)
		{
			$XML = '
				<?xml version="1.0" encoding="utf-8"?>
				<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
					<soap:Header>
						<From memberName="' . $this->user . '"
						    friendlyName="=?utf-8?B?' . base64_encode($this->user) . '?="
						    xml:lang="zh-TW"
						    proxy="MSNMSGR"
						    xmlns="http://messenger.msn.com/ws/2004/09/oim/"
						    msnpVer="MSNP15"
						    buildVer="8.1.0178"/>
						<To memberName="'.$to.'" xmlns="http://messenger.msn.com/ws/2004/09/oim/"/>
						<Ticket passport="' . htmlspecialchars($this->oim_ticket) . '" appid="' . $this->prod_id . '" lockkey="' . $lockkey . '" xmlns="http://messenger.msn.com/ws/2004/09/oim/"/>
						<Sequence xmlns="http://schemas.xmlsoap.org/ws/2003/03/rm">
							<Identifier xmlns="http://schemas.xmlsoap.org/ws/2002/07/utility">
								http://messenger.msn.com
							</Identifier>
							<MessageNumber>
								1
							</MessageNumber>
						</Sequence>
					</soap:Header>
					<soap:Body>
						<MessageType xmlns="http://messenger.msn.com/ws/2004/09/oim/">
							text
						</MessageType>
						<Content xmlns="http://messenger.msn.com/ws/2004/09/oim/">
							MIME-Version: 1.0
							Content-Type: text/plain; charset=UTF-8
							Content-Transfer-Encoding: base64
							X-OIM-Message-Type: OfflineMessage
							X-OIM-Run-Id: {DAB68CFA-38C9-449B-945E-38AFA51E50A7}
							X-OIM-Sequence-Num: 1
							' . chunk_split(base64_encode($sMessage)) . '
						</Content>
					</soap:Body>
				</soap:Envelope>
			';

			$header_array = array(
				'SOAPAction: http://messenger.live.com/ws/2006/09/oim/Store2',
				'Content-Type: text/xml',
				'User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; Messenger 8.1.0178)'
			);

			//$this->debug_message("*** URL: $this->oim_send_url");
			//$this->debug_message("*** Sending SOAP:\n$XML");
			$curl = curl_init();
			curl_setopt($curl, CURLOPT_URL, 'https://ows.messenger.msn.com/OimWS/oim.asmx');
			curl_setopt($curl, CURLOPT_HTTPHEADER, $header_array);
			curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
			curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
			curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
			if($this->debug)
			{
				curl_setopt($curl, CURLOPT_HEADER, 1);
			}
			curl_setopt($curl, CURLOPT_POST, 1);
			curl_setopt($curl, CURLOPT_POSTFIELDS, $XML);
			$data = curl_exec($curl);
			$http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
			curl_close($curl);
			//$this->debug_message("*** Get Result:\n$data");

			if($http_code == 200)
			{
				//$this->debug_message("*** OIM sent for $to");
				return true;
			}

			$challenge = false;
			$auth_policy = false;
			// the lockkey is invalid, authenticated fail, we need challenge it again
			// <LockKeyChallenge xmlns="http://messenger.msn.com/ws/2004/09/oim/">364763969</LockKeyChallenge>
			preg_match("#<LockKeyChallenge (.*)>(.*)</LockKeyChallenge>#", $data, $matches);
			if(count($matches) != 0)
			{
				// yes, we get new LockKeyChallenge
				$challenge = $matches[2];
				$this->debug_message("*** OIM need new challenge ($challenge) for $to");
			}
			// auth policy error
			// <RequiredAuthPolicy xmlns="http://messenger.msn.com/ws/2004/09/oim/">MBI_SSL</RequiredAuthPolicy>
			preg_match("#<RequiredAuthPolicy (.*)>(.*)</RequiredAuthPolicy>#", $data, $matches);
			if(count($matches) != 0)
			{
				$auth_policy = $matches[2];
				$this->debug_message("*** OIM need new auth policy ($auth_policy) for $to");
			}
			if($auth_policy === false && $challenge === false)
			{
				//<faultcode xmlns:q0="http://messenger.msn.com/ws/2004/09/oim/">q0:AuthenticationFailed</faultcode>
				preg_match("#<faultcode (.*)>(.*)</faultcode>#", $data, $matches);
				if(count($matches) == 0)
				{
					// no error, we assume the OIM is sent
					$this->debug_message("*** OIM sent for $to");
					return true;
				}
				$err_code = $matches[2];

				preg_match("#<faultstring>(.*)</faultstring>#", $data, $matches);
				if(count($matches) > 0)
				{
					$err_msg = $matches[1];
				}
				else
				{
					$err_msg = '';
				}
				$this->debug_message("*** OIM failed for $to");
				$this->debug_message("*** OIM Error code: $err_code");
				$this->debug_message("*** OIM Error Message: $err_msg");
				return false;
			}
			return array('challenge' => $challenge, 'auth_policy' => $auth_policy);
		}


		// read data for specified size
		private function readdata($size)
		{
			$data = '';
			$count = 0;
			while(!feof($this->fp))
			{
				$buf = @fread($this->fp, $size - $count);
				$data .= $buf;
				$count += strlen($buf);
				if($count >= $size)
				{
					break;
				}
			}
			//$this->debug_message("NS: data ($size/$count) <<<\n$data");
			return $data;
		}


		// read one line
		private function readln()
		{
			$data = @fgets($this->fp, 4096);
			if($data !== false)
			{
				$data = trim($data);
				//$this->debug_message("NS: <<< $data");
			}
			return $data;
		}

		// write to server, append \r\n, also increase id
		private function writeln($data)
		{
			@fwrite($this->fp, $data."\r\n");
			//$this->debug_message("NS: >>> $data");
			$this->id++;
			return;
		}

		// write data to server
		private function writedata($data)
		{
			@fwrite($this->fp, $data);
			//$this->debug_message("NS: >>> $data");
			return;
		}

		// read data for specified size for SB
		private function sb_readdata($size)
		{
			$data = '';
			$count = 0;
			while(!feof($this->sb))
			{
				$buf = @fread($this->sb, $size - $count);
				$data .= $buf;
				$count += strlen($buf);
				if($count >= $size)
				{
					break;
				}
			}
			//$this->debug_message("SB: data ($size/$count) <<<\n$data");
			return $data;
		}

		// read one line for SB
		private function sb_readln()
		{
			$data = @fgets($this->sb, 4096);
			if($data !== false)
			{
				$data = trim($data);
				//$this->debug_message("SB: <<< $data");
			}
			return $data;
		}

		// write to server for SB, append \r\n, also increase id
		// switchboard server only accept \r\n, it will lost connection if just \n only
		private function sb_writeln($data)
		{
			@fwrite($this->sb, $data."\r\n");
			//$this->debug_message("SB: >>> $data");
			$this->id++;
			return;
		}

		// write data to server
		private function sb_writedata($data)
		{
			@fwrite($this->sb, $data);
			return;
		}

		// show debug information
		private function debug_message($str)
		{
			if($this->debug)
			{
				echo $str."\n";
			}
		}
	}
?>